經過二十多天的開發,GASO (Google Apps Script Odyssey) 的核心功能已經基本完成。我們有了:
但是,當我開始調整前端畫面的美感時,才發現真正的挑戰才剛剛開始。今天,我想分享這段「前端地獄」的經歷,以及從中學到的寶貴教訓。
做前端不像做後端一樣,只要把功能具體描述清楚就好了。做前端難就難在根本就很難講得清楚你要的那個感覺是什麼。
我雖然看得出來某一個設計的樣子很醜,但是你如果問我說:「欸,那比較美麗的樣子是什麼樣子?」我還真的沒辦法用具體的語言講出來。
在 Day 23 中,我們經歷了從現代化設計到古樸風格的轉型。這個過程讓我深刻體會到「感覺」的難以描述:
/* 現代化配色 - 看起來很「科技感」但缺乏溫度 */
background: rgba(52, 152, 219, 0.9);
color: #2c3e50;
/* 古樸配色 - 看起來很「溫暖」但需要精確調整 */
background: #8b4513;
color: #f4f1e8;
問題是:什麼是「科技感」?什麼是「溫暖」?這些都是主觀的感受,很難用程式碼來量化。
我學會了建立視覺參考系統:
/* 建立設計系統 */
:root {
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  --font-family: 'Times New Roman', 'Georgia', serif;
  --border-radius: 4px;
  --shadow: 0 4px 16px rgba(139, 69, 19, 0.3);
}
前端還有一個很難的地方就在於每一個人的螢幕大小、解析度又不一樣。
在 GASO 的開發過程中,我們遇到了各種螢幕尺寸的問題:
/* 桌面版設計 */
.floating-zoom-controls {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 40px;
  height: 40px;
}
/* 手機版適配 */
@media (max-width: 768px) {
  .floating-zoom-controls {
    bottom: 10px;
    right: 10px;
    width: 36px;
    height: 36px;
  }
}
但是,這只是開始。我們還需要考慮:
我學會了使用漸進式增強的方法:
/* 基礎樣式 - 適用於所有設備 */
.floating-zoom-controls {
  position: fixed;
  bottom: 20px;
  right: 20px;
  min-width: 40px;
  min-height: 40px;
}
/* 大螢幕優化 */
@media (min-width: 1200px) {
  .floating-zoom-controls {
    bottom: 30px;
    right: 30px;
    width: 50px;
    height: 50px;
  }
}
/* 小螢幕優化 */
@media (max-width: 768px) {
  .floating-zoom-controls {
    bottom: 10px;
    right: 10px;
    width: 36px;
    height: 36px;
  }
}
如果裡面有多個容器,一層包一層,每一個容器裡面的東西,它又有不同的參考座標,然後又會有不同的縮放比例。
在 Day 24 中,我們深入探討了座標系統的問題。這是一個極其複雜的多層系統:
// 多層座標系統
1. SVG 內部座標系統:Graphviz 生成的節點座標
2. CSS Transform 座標系統:zoomInner 的 translate 和 scale
3. 螢幕像素座標系統:瀏覽器視窗的實際像素
4. 容器座標系統:#graph 容器的尺寸和位置
// 嘗試置中節點的複雜計算
function centerNode(nodeId) {
  // 1. 取得節點在 SVG 中的位置
  const bbox = targetNodeElement.getBBox();
  const nodeX = bbox.x + bbox.width / 2;
  const nodeY = bbox.y + bbox.height / 2;
  
  // 2. 計算縮放後的位置
  const scale = state.scalePct / 100;
  const scaledNodeX = nodeX * scale;
  const scaledNodeY = nodeY * scale;
  
  // 3. 計算需要移動的距離
  const moveX = containerCenterX - scaledNodeX;
  const moveY = containerCenterY - scaledNodeY;
  
  // 4. 應用位移
  state.currentTranslateX = moveX;
  state.currentTranslateY = moveY;
}
這個看似簡單的功能,卻讓我陷入了座標系統的地獄,最終不得不採用更簡單的拖曳方案。
最痛苦的是我常常貪心,一次想要改很多個地方,結果一改就改壞了,就有一些功能壞掉,但我也不知道到底是我改了哪一個部分影響到的。
在 Day 23 中,我同時進行了多項改動:
結果導致:
我學會了「小步快跑」的開發方式:
# 使用 Git 進行小步提交
git add .
git commit -m "feat: 更新配色方案"
# 測試功能
git add .
git commit -m "feat: 調整布局結構"
# 測試功能
我就是這樣子前進後退,前進後退,前進五步,退後三步,然後前進三步又退後了五步。
在縮放功能的開發中,我經歷了以下循環:
第一次嘗試:直接使用 transform: scale()
第二次嘗試:設定容器尺寸 + transform: scale()
第三次嘗試:移除容器尺寸,只使用 transform: scale()
第四次嘗試:簡化為拖曳功能
我學會了建立詳細的開發日誌:
## 縮放功能開發日誌
### 2024-01-15 第一次嘗試
- 方法:直接使用 transform: scale()
- 問題:地圖變得過小
- 原因:沒有考慮容器尺寸
- 解決:回滾
### 2024-01-16 第二次嘗試
- 方法:設定容器尺寸 + transform: scale()
- 問題:雙重縮放
- 原因:同時設定了尺寸和縮放
- 解決:回滾
### 2024-01-17 第三次嘗試
- 方法:移除容器尺寸,只使用 transform: scale()
- 問題:座標計算錯誤
- 原因:座標系統複雜
- 解決:回滾
### 2024-01-18 第四次嘗試
- 方法:簡化為拖曳功能
- 結果:成功
- 原因:避開了複雜的座標計算
這個專案在一開始做單純的功能的時候都很順,每天就做一些,每天就做一些,但是到了要調整前端畫面的美感的時候,根本就是三五天也調不了一點點。
| 階段 | 時間投入 | 難度 | 成就感 | 
|---|---|---|---|
| 功能開發 | 1-2天/功能 | 中等 | 高 | 
| 美學調整 | 3-5天/調整 | 高 | 低 | 
功能開發階段(Day 9):
// 搜尋功能的核心邏輯
function searchNodes(query) {
  const results = state.nodeDetails.filter(node => 
    node.title.toLowerCase().includes(query.toLowerCase())
  );
  displaySearchResults(results);
}
美學調整階段(Day 23-25):
/* 搜尋框的樣式調整 */
.search-container {
  background: linear-gradient(135deg, #f4f1e8 0%, #e8e0d0 100%);
  border: 2px solid #8b4513;
  box-shadow: 0 4px 16px rgba(139, 69, 19, 0.3);
  border-radius: 8px;
  padding: 12px;
}
在開發過程中,我發現自己缺乏一致的設計標準,導致:
/* 建立完整的設計系統 */
:root {
  /* 色彩系統 */
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  --text-color: #654321;
  --background-color: #faf8f3;
  
  /* 字體系統 */
  --font-family-primary: 'Times New Roman', 'Georgia', serif;
  --font-family-secondary: -apple-system, BlinkMacSystemFont, sans-serif;
  --font-size-small: 12px;
  --font-size-medium: 14px;
  --font-size-large: 16px;
  --font-size-xl: 20px;
  
  /* 間距系統 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;
  
  /* 陰影系統 */
  --shadow-sm: 0 2px 4px rgba(139, 69, 19, 0.1);
  --shadow-md: 0 4px 16px rgba(139, 69, 19, 0.3);
  --shadow-lg: 0 8px 32px rgba(139, 69, 19, 0.4);
}
當功能出現問題時,我常常不知道問題出在哪裡,只能盲目地修改程式碼。
// 建立詳細的診斷系統
function debugContainerSizes() {
  const graph = document.getElementById('graph');
  const zoomInner = document.getElementById('zoomInner');
  
  console.log('=== 容器尺寸診斷 ===');
  console.log('Graph 容器:', {
    clientWidth: graph.clientWidth,
    clientHeight: graph.clientHeight,
    offsetWidth: graph.offsetWidth,
    offsetHeight: graph.offsetHeight
  });
  
  console.log('ZoomInner 容器:', {
    clientWidth: zoomInner.clientWidth,
    clientHeight: zoomInner.clientHeight,
    transform: zoomInner.style.transform
  });
}
// 在關鍵時刻調用診斷
function centerNode(nodeId) {
  console.log('=== 開始置中節點 ===');
  debugContainerSizes();
  
  // ... 置中邏輯
  
  setTimeout(() => {
    console.log('=== 置中後驗證 ===');
    debugContainerSizes();
  }, 100);
}
每次修改後,我都不確定哪些功能可能受到影響。
## 功能測試清單
### 基本功能
- [ ] 地圖載入正常
- [ ] 節點顯示正確
- [ ] 連線顯示正確
### 互動功能
- [ ] 節點點擊正常
- [ ] 搜尋功能正常
- [ ] 路徑高亮正常
### 縮放功能
- [ ] 放大按鈕正常
- [ ] 縮小按鈕正常
- [ ] 重設縮放正常
- [ ] 觀看全地圖正常
### 拖曳功能
- [ ] 地圖拖曳正常
- [ ] 拖曳邊界限制正常
- [ ] 拖曳後縮放正常
### 響應式設計
- [ ] 桌面版顯示正常
- [ ] 平板版顯示正常
- [ ] 手機版顯示正常
我常常追求完美的解決方案,結果讓問題變得更加複雜。
在 Day 24 中,我學會了妥協:
// 原本想要的自動置中功能(複雜)
function centerNode(nodeId) {
  // 複雜的座標計算
  const bbox = node.getBBox();
  const nodeX = bbox.x + bbox.width / 2;
  const nodeY = bbox.y + bbox.height / 2;
  // ... 更多複雜計算
}
// 最終採用的拖曳方案(簡單)
document.addEventListener('mousedown', function(e) {
  if (e.target.closest('svg') && !e.target.closest('g.node')) {
    startDrag(e);
  }
});
有時候,簡單的解決方案比複雜的完美方案更好。
/* 使用 CSS 變數提高可維護性 */
:root {
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
}
.button {
  background-color: var(--primary-color);
  color: var(--secondary-color);
}
/* 移動優先的響應式設計 */
.container {
  padding: 16px;
}
@media (min-width: 768px) {
  .container {
    padding: 24px;
  }
}
@media (min-width: 1200px) {
  .container {
    padding: 32px;
  }
}
<!-- 使用語義化的 HTML 結構 -->
<header class="header">
  <h1>GASO - Google Apps Script Odyssey</h1>
  <nav class="navigation">
    <button class="search-btn">搜尋</button>
  </nav>
</header>
<main class="main-content">
  <div class="graph-container" id="graph">
    <!-- 地圖內容 -->
  </div>
</main>
<aside class="sidebar">
  <!-- 側邊面板 -->
</aside>
// 將功能分離到不同的模組
const SearchModule = {
  init() {
    this.bindEvents();
  },
  
  bindEvents() {
    document.getElementById('search-btn').addEventListener('click', this.handleSearch.bind(this));
  },
  
  handleSearch(event) {
    // 搜尋邏輯
  }
};
const ZoomModule = {
  init() {
    this.bindEvents();
  },
  
  bindEvents() {
    document.getElementById('zoom-in').addEventListener('click', this.zoomIn.bind(this));
  },
  
  zoomIn() {
    // 縮放邏輯
  }
};
// 使用事件委託提高效能
document.addEventListener('click', function(e) {
  if (e.target.matches('.zoom-btn')) {
    handleZoom(e.target);
  } else if (e.target.matches('.node')) {
    handleNodeClick(e.target);
  }
});
// 建立統一的狀態管理
const AppState = {
  scalePct: 100,
  currentTranslateX: 0,
  currentTranslateY: 0,
  nodeDetails: [],
  
  updateScale(newScale) {
    this.scalePct = newScale;
    this.notify('scaleChanged', newScale);
  },
  
  updateTranslate(x, y) {
    this.currentTranslateX = x;
    this.currentTranslateY = y;
    this.notify('translateChanged', { x, y });
  },
  
  notify(event, data) {
    // 通知其他模組狀態變化
  }
};
// 建立除錯模式
const DEBUG = true;
function debugLog(message, data) {
  if (DEBUG) {
    console.log(`[DEBUG] ${message}`, data);
  }
}
function centerNode(nodeId) {
  debugLog('開始置中節點', { nodeId });
  
  // ... 置中邏輯
  
  debugLog('置中完成', { 
    finalX: state.currentTranslateX, 
    finalY: state.currentTranslateY 
  });
}
// 建立視覺化除錯工具
function showDebugInfo() {
  const debugPanel = document.createElement('div');
  debugPanel.id = 'debug-panel';
  debugPanel.style.cssText = `
    position: fixed;
    top: 10px;
    left: 10px;
    background: rgba(0,0,0,0.8);
    color: white;
    padding: 10px;
    font-family: monospace;
    font-size: 12px;
    z-index: 9999;
  `;
  
  document.body.appendChild(debugPanel);
  
  setInterval(() => {
    const graph = document.getElementById('graph');
    const zoomInner = document.getElementById('zoomInner');
    
    debugPanel.innerHTML = `
      縮放比例: ${state.scalePct}%<br>
      位移: (${state.currentTranslateX}, ${state.currentTranslateY})<br>
      容器尺寸: ${graph.clientWidth}x${graph.clientHeight}<br>
      SVG 尺寸: ${zoomInner.clientWidth}x${zoomInner.clientHeight}
    `;
  }, 100);
}
在開始開發之前,先建立完整的設計系統:
/* 在開始開發前就定義好設計系統 */
:root {
  /* 色彩系統 */
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  
  /* 字體系統 */
  --font-family: 'Times New Roman', 'Georgia', serif;
  --font-size-base: 14px;
  
  /* 間距系統 */
  --spacing-unit: 8px;
  --spacing-xs: calc(var(--spacing-unit) * 0.5);
  --spacing-sm: var(--spacing-unit);
  --spacing-md: calc(var(--spacing-unit) * 2);
  --spacing-lg: calc(var(--spacing-unit) * 3);
  --spacing-xl: calc(var(--spacing-unit) * 4);
}
/* 定義響應式斷點 */
:root {
  --breakpoint-sm: 576px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 992px;
  --breakpoint-xl: 1200px;
}
@media (min-width: var(--breakpoint-md)) {
  /* 平板樣式 */
}
@media (min-width: var(--breakpoint-lg)) {
  /* 桌面樣式 */
}
# 每次只做一個小改動
git add .
git commit -m "feat: 更新按鈕顏色"
# 測試功能
git add .
git commit -m "feat: 調整按鈕間距"
# 測試功能
## 每次改動後的測試清單
- [ ] 基本功能正常
- [ ] 響應式設計正常
- [ ] 不同瀏覽器正常
- [ ] 效能沒有明顯下降
# 建立功能分支
git checkout -b feature/new-design
# 進行改動
git add .
git commit -m "feat: 新設計"
# 測試無誤後合併
git checkout main
git merge feature/new-design
// 建立除錯工具
const DebugTools = {
  showContainerInfo() {
    const containers = ['graph', 'zoomInner', 'header'];
    containers.forEach(id => {
      const el = document.getElementById(id);
      if (el) {
        console.log(`${id}:`, {
          clientWidth: el.clientWidth,
          clientHeight: el.clientHeight,
          offsetWidth: el.offsetWidth,
          offsetHeight: el.offsetHeight,
          transform: el.style.transform
        });
      }
    });
  },
  
  showStateInfo() {
    console.log('App State:', {
      scalePct: state.scalePct,
      currentTranslateX: state.currentTranslateX,
      currentTranslateY: state.currentTranslateY,
      nodeDetails: state.nodeDetails.length
    });
  }
};
// 使用防抖函數優化搜尋
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}
const debouncedSearch = debounce(searchNodes, 300);
// 清理事件監聽器
function cleanup() {
  document.removeEventListener('click', handleClick);
  document.removeEventListener('resize', handleResize);
}
// 在頁面卸載時清理
window.addEventListener('beforeunload', cleanup);
經過這二十多天的開發,我深刻體會到前端開發的複雜性和挑戰性。從功能實現到視覺美學,每一步都需要仔細的規劃和執行。
前端開發不僅是技術的實現,更是藝術與科學的結合。它需要我們:
在這個過程中,我們會遇到挫折,會感到困惑,但每一次的挑戰都是成長的機會。當我們最終看到一個美觀、實用的介面時,所有的努力都是值得的。
前端開發的智慧,不在於追求完美,而在於在限制中找到最佳的平衡點。
在 GASO 的開發旅程中,每一天都是新的學習,每一次挑戰都是成長的機會。讓我們帶著今天的智慧,繼續前進!🚀